using System;
using System.Collections.Generic;
using System.Linq;
using TestCases.CompareAssert.Core.Comparers.Interfaces;
using TestCases.CompareAssert.Core.Interfaces;

namespace TestCases.CompareAssert.Core.Comparers.Collections
{
    public class ArraysComparer: IObjectComparer
    {

        public const string IdentifiableName = "ArraysComparer";
        public const string EqualsMessage = "All <{LPath}> and <{RPath}> elements are equals [{LValue} == {RValue}].";

        public ArraysComparer()
        {            
        }
        
        public ArraysComparer(IObjectComparer elementComparer)
        {
            ElementComparer = elementComparer;
        }

        public IObjectComparer ElementComparer { get; set; }
        
        public Predicate<long[]> IgnoreElementAt { get; set; }

        public string Name
        {
            get { return IdentifiableName; }
        }

        public IResultBuilder Compare(ICompareContext context)
        {
            
            if (!(context.LValue is Array))
            {
                return this.IsNotApplicable(context).Left.IsNot<Array>();
            }
            
            if (!(context.RValue is Array))
            {
                return this.IsNotApplicable(context).Right.IsNot<Array>();
            }

            var leftArray = (Array)context.LValue;
            var rightArray = (Array)context.RValue;

            
            if (leftArray.Rank != rightArray.Rank)
            {
                return this.IsNotEquals(context).
                    Message("Arrays <{LPath}> and <{RPath}> has different ranks [{LRankValue} != {RRankValue}].").
                    With(new {LRankValue = leftArray.Rank, RRankValue = rightArray.Rank});                
            }

            long lastLength = 0L;
            int rank = leftArray.Rank, lastDimension = rank-1;
            for (int dimension = 0; dimension < rank; dimension++)
            {
                long leftLength = leftArray.GetLongLength(dimension), rightLength = rightArray.GetLongLength(dimension);
                if (leftLength != rightLength)
                {
                    return this.IsNotEquals(context).
                        Message("<{LPath}> and <{RPath}> has different lengths at {Dimension} dimension [{LLength} != {RLength}].").
                        With(new {Dimension = dimension, LLength = leftLength, RLength = rightLength});
                }                
                if (dimension == lastDimension)
                {
                    lastLength = leftLength;
                }
            }
                        
            if (lastLength == 0L)
            {
                return this.IsEquals(context).Message(EqualsMessage);
            }
            
            var indexes = new long[rank];
            var results = new List<IResultProvider>();
            return CompareArrays(context, leftArray, rightArray, indexes, results, 0) 
                ?? this.IsEquals(context, results).Message(EqualsMessage);
        }

        private IResultBuilder CompareArrays(ICompareContext context, Array leftArray, Array rightArray, long[] indexes, IList<IResultProvider> results, int dimension)
        {
            long length = leftArray.GetLongLength(dimension);
            int lastDimension = indexes.Length-1;
            
            if (dimension == lastDimension)
            {                           
                for (indexes[dimension] = 0; indexes[dimension] < length; indexes[dimension]++)
                {
                    if (IgnoreElementAt != null && IgnoreElementAt(indexes))
                    {
                        continue;
                    }

                    var leftValue = leftArray.GetValue(indexes);
                    var rightValue = rightArray.GetValue(indexes);

                    var indexedContext = context.ForIndexedObjects(leftValue, rightValue, indexes.Cast<object>().ToArray());
                    if (ElementComparer.CompareAndAddResult(indexedContext, results).GetStatus() == CompareStatus.NotEquals)
                    {
                        return this.IsNotEquals(context, results).
                            Message("<{LPath}> and <{RPath}> elements at {Indexes} are not equals. [{LValue} != {RValue}].").
                            With(new {Indexes = indexes});
                    }
                }
            }
            else
            {
                for (indexes[dimension] = 0; indexes[dimension] < length; indexes[dimension]++)
                {
                    var result = CompareArrays(context, leftArray, rightArray, indexes, results, dimension + 1);
                    if (result != null)
                    {
                        return result;
                    }            
                }                
            }                        
            return null;
        }
    }
}